/*
 * Run jobs in parallel
 *
 * Copyright 2023 and 2024 Odin Kroeger.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <https://www.gnu.org/licenses/>.
 */

#define _BSD_SOURCE
#define _DARWIN_C_SOURCE
#define _DEFAULT_SOURCE
#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <spawn.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>


/*
 * Constants
 */

/* Base 10. */
#define BASE_TEN 10

/* Control sequence for clearing the current line */
#define CLEARLN "\033" "[0K"

/* Default time that jobs may take to terminate (in secs) */
#ifndef DEF_GRACE
#define DEF_GRACE 5.0
#else
#if DEF_GRACE < DBL_MIN
#error DEF_GRACE is smaller than DBL_MIN
#elif DEF_GRACE > DBL_MAX
#error DEF_GRACE is greater than DBL_MAX
#endif
#endif

/* Default number of jobs to run in parallel */
#ifndef DEF_NRUN
#define DEF_NRUN 4
#else
#if DEF_NRUN < PTRDIFF_MIN
#error DEF_NRUN is smaller than PTRDIFF_MIN
#elif DEF_NRUN > PTRDIFF_MAX
#error DEF_NRUN is greater than PTRDIFF_MAX
#endif
#endif

/* Default timeout (in secs), where 0 means "forever" */
#ifndef DEF_TIMEOUT
#define DEF_TIMEOUT 0
#else
#if DEF_TIMEOUT < LONG_MIN
#error DEF_TIMEOUT is smaller than LONG_MIN
#elif DEF_TIMEOUT > LONG_MAX
#error DEF_TIMEOUT is greater than LONG_MAX
#endif
#endif

/* Exit status for usage errors */
#define EXIT_USAGE 2

/* Highest non-signal exit status */
#define MAX_NEXIT 128

/* Maximum number of jobs to run in parallel */
#ifndef MAX_NRUN
#define MAX_NRUN 64
#else
#if MAX_NRUN < 1
#error MAX_NRUN must not be smaller than one
#elif MAX_NRUN > PTRDIFF_MAX
#error MAX_NRUN is greater than PTRDIFF_MAX
#endif
#endif

/* Number of words to allocate memory for initially */
#define MAX_NWORDS 32U

/* Maximum number of environment variables */
#ifndef MAX_NVARS
#define MAX_NVARS 256
#else
#if MAX_NVARS < 1
#error MAX_NVARS must not be smaller than one
#elif MAX_NVARS > PTRDIFF_MAX
#error MAX_NVARS is greater than PTRDIFF_MAX
#endif
#endif

/* Maximum depth of nested quotes for word-splitting */
#ifndef MAX_QUOT_NEST
#define MAX_QUOT_NEST 16
#else
#if MAX_QUOT_NEST < 4
#error MAX_QUOT_NEST must not be smaller than four
#elif MAX_QUOT_NEST > PTRDIFF_MAX
#error MAX_QUOT_NEST is greater than PTRDIFF_MAX
#endif
#endif

/* Maximum string size, including the null terminator */
#ifndef MAX_STR_SIZE
#define MAX_STR_SIZE 4096U
#else
#if MAX_STR_SIZE < 256
#error MAX_STR_SIZE must not be smaller than 256
#elif MAX_STR_SIZE > SIZE_MAX
#error MAX_STR_SIZE is greater than SIZE_MAX
#endif
#endif

/* Program name */
#define PROGNAME "para"

/* Control sequence for moving to the beginning of the previous line */
#define REWINDLN "\r" "\033" "[1A"

/* Version */
#define VERSION "0.8.3a"


/*
 * Macros
 */

/* Only use C attributes if GNU C is supported */
#if !defined(__GNUC__) || __GNUC__ < 3
/* cppcheck-suppress misra-c2012-21.1; idiomatic */
#define __attribute__(attr)
#endif

/* Calculate the number of elements in an array */
#define NELEMS(array) (sizeof(array) / sizeof(*(array)))


/*
 * Data types
 */

/* Errors */
typedef enum {
    OK = 0,                         /* Success */
    ERR_ARGS,                       /* Argument invalid */
    ERR_BAD,                        /* Bad input */
    ERR_LEN,                        /* String too long */
    ERR_NELEMS,                     /* Too many elements */
    ERR_OVER,                       /* Value would overflow */
    ERR_QUOTES,                     /* Quotes nested too deeply */
    ERR_SIGN,                       /* Sign change */
    ERR_SYNTAX,                     /* Syntax error */
    ERR_SYS,                        /* Systen error; errno should be set */
    ERR_UNSPEC                      /* Unspecified error */
} Error;

/* Record for started jobs */
typedef struct {
    pid_t pid;                      /* Process ID */
    char *comm;                     /* Command */
    int status;                     /* Exit status */
    volatile sig_atomic_t exited;   /* Has the job has exited? */
} Job;

/* Signal handler specification */
typedef struct {
    int signo;                      /* Signal to catch */
    int flags;                      /* sigaction flags */
    void (* handler)(int);          /* Signal handler */
} Trap;

/* Array of words */
typedef struct {
    char **vector;                  /* Words */
    size_t count;                   /* Word count */
    size_t max;                     /* Number of words the array can hold */
} Words;


/*
 * Prototypes
 */

/*
 * Store the given signal in the global "caught".
 *
 * Globals:
 *      caught      Most recently caught signal.
 */
static void catch(int signo);

/*
 * Reset the alarm, send a TERM to all running jobs, wait "grace" seconds
 * for them to terminate, and then kill any jobs that are still running.
 *
 * Side-effects:
 *     May print warnings to standard error.
 *
 * Globals:
 *      grace   Timeout.
 *
 * Caveats:
 *     Not async-safe.
 */
static void cleanup(void);

/*
 * Clear the current line.
 *
 * Caveats:
 *     Not async-safe.
 */
static void clearln(FILE *tty);

/*
 * Collect all finished jobs and store that they have exited and the
 * status that they have exited with in the global "jobs". If waitpid
 * raises an error other than ECHILD, store that error in WAITPIDERR; if
 * a process ID cannot be found, store that process ID in "unknownpid".
 *
 * Globals:
 *      jobs        Running jobs.
 *      waitpiderr  Most recent error returned by waitpid.
 *      unknownpid  Most recently collected unknown process ID.
 *
 * Caveats:
 *     Not async-safe.
 */
static void collect(int signo);

/*
 * Convert the current option argument into a number. If the argument is not
 * a number, smaller than the given minimum, or greater than the given maximum,
 * an error message is printed to standard error and the process is aborted.
 *
 * Side-effects:
 *      May terminate the process and write a message to standard error.
 *
 * Globals:
 *      optarg  Current option argument.
 *
 * Caveats:
 *      Not async-safe.
 */
__attribute__((warn_unused_result))
static long getoptl(long min, long max);

/*
 * Get the current time.
 */
__attribute__((warn_unused_result))
static time_t now(void);

/*
 * Add a variable to a NULL-terminated array of variables. If a variable of
 * the same name has already been set, that variable is replaced; otherwise,
 * the variable is added to the end of the array and the counter pointed to
 * by "nvars" is incremented. "nvars" must point to a counter that holds
 * the current number of variables in "vars", sans the NULL terminator.
 *
 * Caveats:
 *      "vars" must have space for the variable to be added.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Invalid parameter.
 *      ERR_BAD     Malformed variable.
 *      ERR_OVER    Variable name is too long.
 */
__attribute__((warn_unused_result))
static Error putvar(ptrdiff_t *nvars, char **vars, char *var);

/*
 * Add a word to the given NULL-terminated array of words.
 * The array is resized if needed, keeping the NULL terminator.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Invalid parameter.
 *      ERR_OVER    Too many words.
 *      ERR_SYS     realloc failed; errno should be set.
 */
__attribute__((warn_unused_result))
static Error wordsadd(Words *words, char *word);

/*
 * Free the memory used by the given array of words.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Invalid parameter.
 *      ERR_BAD     Too many variables.
 */
static Error wordsfree(Words *words);

/*
 * Initialise the given array of words.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Parameter invalid.
 *      ERR_SYS     calloc failed; errno should be set.
 */
__attribute__((warn_unused_result))
static Error wordsinit(Words *words, size_t count);

/*
 * Resize the memory area pointed to by the given array of words
 * so that it can hold the given number of words, which must account
 * for the NULL terminator.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Parameter invalid.
 *      ERR_OVER    Too many words.
 *      ERR_SYS     realloc failed; errno should be set.
 */
static Error wordsrealloc(Words *words, size_t count);

/*
 * Split the given string, which must be of the given length,
 * into words and save the resulting words to given array.
 * The array is terminated with NULL.
 *
 * Return value:
 *      OK          Success.
 *      ERR_ARGS    Parameter invalid.
 *      ERR_LEN     String is too long.
 *      ERR_NELEMS  More words than characters.*
 *      ERR_OVER    Word counter would overflow.*
 *      ERR_QUOTES  Quotes nested too deeply.
 *      ERR_SIGN    Negative word length.*
 *      ERR_SYS     Memory allocation error; errno should be set.
 *      ERR_UNSPEC  Unspeficied error.*
 *
 *      * Should be impossible.
 *
 * Caveats:
 *      The array MUST be uninitialised.
 */
__attribute__((warn_unused_result))
static Error wordsplit(Words *words, size_t slen, const char *str);

/*
 * Rewind to the beginning of the previous line.
 *
 * Caveats:
 *     Not async-safe.
 */
static void rewindln(FILE *tty);

/*
 * Send the given signal to all running jobs.
 *
 * Return value:
 *      Number of jobs signalled.
 *
 * Side-effects:
 *      May print warnings to standard error.
 *
 * Caveats:
 *      Not async-safe.
 */
static int signaljobs(int signo);


/*
 * Global variables
 */

extern char **environ;


/*
 * Module variables
 */

/* Most recently started jobs */
static Job jobs[MAX_NRUN] = {0};

/* Most recently caught signal */
static volatile sig_atomic_t caught = 0;

/* Number of running jobs */
static volatile sig_atomic_t nrun = 0;

/* Most recent unexpected waitpid error */
static volatile sig_atomic_t waitpiderr = 0;

/* Most recent unknown process ID */
static volatile sig_atomic_t unknownpid = 0;

/* Maximum number of jobs to run in parallel */
static ptrdiff_t maxnrun = DEF_NRUN;

/* Signal set that contains only CHLD */
static sigset_t chldsig;

/* Number of seconds to wait for jobs to terminate on abnormal exit */
static double grace = DEF_GRACE;


/*
 * Main
 */

int
main (int argc, char **argv)
{
    int retval = EXIT_SUCCESS;          /* Exit status */


    /*
     * Options
     */

    bool statusmask[MAX_NEXIT] = {0};   /* Ignored exit statuses */
    long timeout = DEF_TIMEOUT;         /* Timeout in seconds */
    bool dontstop = false;              /* Continue despite errors? */
    bool quiet = false;                 /* Be quiet? */
    bool jobout = false;                /* Show job output? */
    int opt;                            /* Option character */

    /* RATS: ignore; para is not security-critical */
    while ((opt = getopt(argc, argv, "Vcg:j:i:qt:sh")) != -1) {
        switch (opt) {
        case 'h':
            (void) printf(
"para - run jobs in parallel\n\n"
"Usage: para [-c] [-g n] [-i status] [-j n] [-t n] [-q] [-s]\n"
"            [name=value ...] command [...]\n"
"       para -h\n\n"
"Operands:\n"
"    name=value  Variable to add to the environment.\n"
"    command     Command to run. Subject to shell-like word splitting.\n\n"
"Options:\n"
"    -V          Show version and legal information.\n"
"    -c          Continue even if a job exited with a non-zero status.\n"
"    -g n        Give jobs n secs to terminate when aborted (default: %.0f).\n"
"    -i n        Do not treat non-zero exit status n as error.\n"
"    -j n        Run up to n jobs in parallel (default: %lld).\n"
"    -t n        Abort after n secs (default: no timeout).\n"
"    -q          Be quiet.\n"
"    -s          Show job output.\n"
"    -h          Print this help screen.\n\n"
"    -i can be given multiple times.\n"
"    -q and -s are implied if only one command is given.\n\n"
"Report bugs to: <https://github.com/odkr/para/issues>\n"
"Home page: <https://odkr.codeberg.page/para>\n",
                grace, (long long) maxnrun);
            return EXIT_SUCCESS;
        case 'V':
            (void) printf(
"%s %s\n"
"Copyright 2023 and 2024 Odin Kroeger.\n"
"Released under the GNU General Public License.\n"
"This program comes with ABSOLUTELY NO WARRANTY.\n",
                PROGNAME, VERSION);
            return EXIT_SUCCESS;
        case 'c':
            dontstop = true;
            break;
        case 'g':
/* This cast should be safe */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wbad-function-cast"
            /* DBL_MIN is < 0 and SHRT_MAX is < DBL_MAX */
            grace = (double) getoptl(0, SHRT_MAX);
#pragma GCC diagnostic pop
            break;
        case 'j':
            /* PTRDIFF_MIN is < 1 and NELEMS(jobs) is < PTRDIFF_MAX */
            maxnrun = (ptrdiff_t) getoptl(1, NELEMS(jobs));
            break;
        case 'i':
            /* PTRDIFF_MIN is < 0 and NELEMS(statusmask) is < PTRDIFF_MAX */
            statusmask[(ptrdiff_t) getoptl(0, NELEMS(statusmask) - 1U)] = true;
            break;
        case 'q':
            quiet = true;
            break;
        case 't':
            timeout = getoptl(1, LONG_MAX);
            break;
        case 's':
            jobout = true;
            break;
        default:
            return EXIT_USAGE;
        }
    }

    argc -= optind; 
    argv += optind; /* cppcheck-suppress misra-c2012-18.4; idiomatic */


    /*
     * Environment
     */

    /* RATS: ignore; writes to vars are bounds-checked */
    char *vars[MAX_NVARS] = {0};        /* Job environment */
    ptrdiff_t nvars = 0;                /* Number of variables */
    int varind = 0;                     /* Last variable in argv */

    for (; nvars < MAX_NVARS && environ[nvars]; ++nvars) {
        vars[nvars] = environ[nvars];
    }

    if (nvars == MAX_NVARS || environ[nvars]) {
        errx(EXIT_FAILURE, "Environment is full");
    }

    for (; varind < argc && argv[varind]; ++varind) {
        const Error pverr = putvar(&nvars, vars, argv[varind]);
        switch (pverr) {
        case OK:
            break;
        case ERR_BAD:
            goto exit; 
        case ERR_OVER:
            errx(EXIT_FAILURE, "Variable name too long");
        default:
            errx(EXIT_FAILURE, "putvar(%p, %p, %s) -> %u [!]",
                 (void *) &nvars, (void *) vars, argv[varind], pverr);
        }

        if (nvars == MAX_NVARS) {
            errx(EXIT_FAILURE, "Too many variables");
        }
    }

    exit:
        /* Loop exit */;

    argc -= varind;
    /* cppcheck-suppress [misra-c2012-10.3, misra-c2012-18.4] */
    argv += varind;


    /*
     * Attributes and file descriptors
     */

    posix_spawnattr_t procattrs;            /* Process attributes */
    posix_spawn_file_actions_t fileacts;    /* File descriptor actions */
    Words comms[MAX_NRUN] = {0};            /* Command vectors */

    errno = posix_spawnattr_init(&procattrs);
    if (errno != 0) {
        err(EXIT_FAILURE, "posix_spawnattr_init");
    }

    errno = posix_spawn_file_actions_init(&fileacts);
    if (errno != 0) {
        err(EXIT_FAILURE, "posix_spawn_file_actions_init");
    }

    if (argc > 1 && !jobout) {
        errno = 0;
        /* RATS: ignore; open is safe */
        const int nullfd = open("/dev/null", O_RDWR | O_NONBLOCK);
        if (nullfd < 0) {
            err(EXIT_FAILURE, "open /dev/null");
        }

        for (int fd = 0; fd < 3; ++fd) {
            errno = posix_spawn_file_actions_adddup2(&fileacts, nullfd, fd);
            if (errno != 0) {
                err(EXIT_FAILURE, "posix_spawn_file_actions_adddup2");
            }
        }
    }


    /*
     * Arguments
     */

    for (int i = 0; i < argc; ++i) {
        const char *const arg = argv[i];
        const size_t arglen = strnlen(arg, MAX_STR_SIZE);
        if (arglen >= MAX_STR_SIZE) {
            errx(EXIT_FAILURE, "Argument %d is too long", i);
        }

        const Error wserr = wordsplit(&comms[i], arglen, arg);
        switch (wserr) {
        case OK:
            break;
        case ERR_LEN:
            errx(EXIT_FAILURE, "wordsplit: String too long");
        case ERR_QUOTES:
            errx(EXIT_FAILURE, "%s: Quotes nested too deeply", arg);
        case ERR_SYNTAX:
            errx(EXIT_FAILURE, "%s: Unbalanced quotes", arg);
        case ERR_SYS:
            err(EXIT_FAILURE, "wordsplit");
        default:
            errx(EXIT_FAILURE, "wordsplit(-> %p, %zu, %s) -> %u [!]",
                 (void *) &comms[i], arglen, arg, (unsigned) wserr);
        }
    }


    /*
     * Signal handling
     */

    /* How to handle which signals */
    const Trap traps[] = {
        {.signo = SIGCHLD, .handler = collect, .flags = SA_RESTART},
        {.signo = SIGHUP, .handler = catch},
        {.signo = SIGINT, .handler = catch},
        {.signo = SIGALRM, .handler = catch},
        {.signo = SIGTERM, .handler = catch}
    };

    /* Default signal handler */
    const struct sigaction defhdl = {
        .sa_handler = SIG_DFL
    };

    sigset_t nosigs;                        /* Empty set */
    sigset_t oldmask;                       /* Old signals */
    sigset_t trapped;                       /* Trapped signals */

    (void) sigemptyset(&nosigs);
    (void) sigemptyset(&chldsig);
    (void) sigemptyset(&trapped);

/* GCC takes chldsig for an int, and thus warns about a sign change */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-conversion"
    errno = 0;
    if (sigaddset(&chldsig, SIGCHLD) != 0) {
        errx(EXIT_FAILURE, "sigaddset");
    }
#pragma GCC diagnostic pop

    errno = 0;
    if (sigprocmask(SIG_BLOCK, &chldsig, &oldmask) != 0) {
        err(EXIT_FAILURE, "sigprocmask");
    }

    for (size_t i = 0; i < NELEMS(traps); ++i) {
        const Trap trap = traps[i];
        const struct sigaction action = {
            .sa_handler = trap.handler,
            .sa_flags = trap.flags,
        };

        errno = 0;
        if (sigaction(trap.signo, &action, NULL) != 0) {
            err(EXIT_FAILURE, "sigaction");
        }

/* GCC takes trapped for an int, and thus warns about a sign change */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-conversion"
        errno = 0;
        if (sigaddset(&trapped, trap.signo) != 0) {
            errx(EXIT_FAILURE, "sigaddset");
        }
#pragma GCC diagnostic pop
    }

    errno = posix_spawnattr_setflags(
        &procattrs, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF
    );

    if (errno != 0) {
        err(EXIT_FAILURE, "posix_spawnattr_setflags");
    }

    errno = posix_spawnattr_setsigmask(&procattrs, &oldmask);
    if (errno != 0) {
        err(EXIT_FAILURE, "posix_spawnattr_setsigmask");
    }

    errno = posix_spawnattr_setsigdefault(&procattrs, &trapped);
    if (errno != 0) {
        err(EXIT_FAILURE, "posix_spawnattr_setsigdefault");
    }


    /*
     * Defaults
     */

    if (argc == 1) {
        quiet = true;
    }


    /*
     * Exit handlers
     */

    errno = 0;
    if (atexit(cleanup) != 0) {
        err(EXIT_FAILURE, "atexit");
    }


    /*
     * Main loop
     */

    int nfork = 0;                          /* Number of jobs forked */
    int nrep = 0;                           /* Number of jobs reported on */

    (void) alarm((unsigned int) timeout);

    do {
        /* Report errors */
        if (waitpiderr > 0) {
            /* NOTREACHED */
            clearln(stderr);
            errx(EXIT_FAILURE, "waitpid: %s", strerror((int) waitpiderr));
        }

        if (unknownpid > 0) {
            /* NOTREACHED */
            clearln(stderr);
            errx(EXIT_FAILURE, "process %ld: Unknown", (long) unknownpid);
        }

        for (ptrdiff_t i = 0; i < maxnrun; ++i) {
            Job *job = &jobs[i];

            /* Report exit status of finished jobs */
            if (job->exited == 1) {
                if (WIFSIGNALED(job->status)) {
                    const int signo = WTERMSIG(job->status);
                    clearln(stderr);
                    errx(EXIT_FAILURE, "%s[%d]: %s",
                         job->comm, job->pid, strsignal(signo));
                } else if (WIFEXITED(job->status)) {
                    const int status = WEXITSTATUS(job->status);
                    if (status != 0) {
                        clearln(stderr);
                        warnx("%s[%d] exited with status %d",
                              job->comm, job->pid, status);

                        if (status < 0) {
                            /* NOTREACHED */
                            return status;
                        }
/* cppcheck-suppress misra-c2012-20.9; INT_MAX and PTRDIFF_T are defined */
#if INT_MAX > PTRDIFF_MAX
                        if ((uintmax_t) status > (uintmax_t) PTRDIFF_MAX) {
                            /* NOTREACHED */
                            return status;
                        }
#endif
/* cppcheck-suppress misra-c2012-20.9; INT_MAX and PTRDIFF_T are defined */
#if INT_MAX > SIZE_MAX
                        if ((uintmax_t) status > (uintmax_t) SIZE_MAX) {
                            /* NOTREACHED */
                            return status;
                        }
#endif

                        if (!statusmask[(ptrdiff_t) status] ||
                            (size_t) status > NELEMS(statusmask))
                        {
                            if (!dontstop) {
                                return status;
                            }

                            retval = status;
                        }
                    }
                } else {
                    /* NOTREACHED */
                    clearln(stderr);
                    errx(EXIT_FAILURE, "%s[%d] exited abnormally",
                         job->comm, job->pid);
                }

                job->pid = 0;
                job->exited = 0;
                ++nrep;
            }

            /* Start the next job */
            if (job->pid == 0 && nfork < argc) {
                char **comm = comms[nfork].vector;

                /* comms and jobs effectively have the same lifetime */
                job->comm = *comm;

                errno = posix_spawnp(&job->pid, *comm, &fileacts,
                                     &procattrs, comm, vars);
                if (errno == 0) {
                    if (!quiet) {
                        clearln(stderr);
                        warnx("[%d] %s", job->pid, argv[nfork]);
                    }

                    ++nfork;
                    ++nrun;
                } else {
                    clearln(stderr);
                    err(EXIT_FAILURE, "posix_spawnp %s", job->comm);
                }
            }
        }

        /* Wait for jobs to finish */
        if (nrun > 0) {
            if (nfork == argc && !quiet) {
                clearln(stderr);
                warnx("Waiting for %lu job%s to finish ...",
                      (unsigned long) nrun, nrun == 1 ? "" : "s");
                rewindln(stderr);
            }

            (void) sigsuspend(&nosigs);
        }
    } while (argc > nrep && caught == 0);


    /*
     * Cleanup
     */

    (void) alarm(0);

    /* 
     * Freeing memory is superfluous at this point,
     * but some versions of ASan think otherwise.
     */
    for (int i = 0; i < argc; ++i) {
        (void) wordsfree(&comms[i]);
    }

    errno = posix_spawn_file_actions_destroy(&fileacts);
    if (errno != 0) {
        clearln(stderr);
        err(EXIT_FAILURE, "posix_spawn_file_actions_destroy");
    }

    if (caught > 0) {
        clearln(stderr);
        warnx("%s", strsignal((int) caught));

        cleanup();

        (void) sigaction((int) caught, &defhdl, NULL);
        (void) raise((int) caught);

        /* NOTREACHED */
        return (int) caught + MAX_NEXIT;
    }

    if (!quiet) {
        clearln(stderr);
        if (argc == 0) {
            warnx("No jobs given");
        } else {
            warnx("%d jobs completed", argc);
        }
    }

    return retval;
}


/*
 * Functions
 */

static void
catch(const int signo) {
    caught = signo;
}


static void
cleanup(void)
{
    (void) alarm(0);

    if (nrun > 0) {
        sigset_t oldmask;
        errno = 0;
        if (sigprocmask(SIG_UNBLOCK, &chldsig, &oldmask) != 0) {
            warn("sigprocmask");
        }

        (void) signaljobs(SIGTERM);

        const time_t start = now();     /* Current time */
        double cnt;                     /* Countdown */
        /* cppcheck-suppress misra-c2012-13.5; cnt is only needed if nrun > 0 */
        while (nrun > 0 && (cnt = difftime(now(), start)) < grace) {
            clearln(stderr);
            warnx("Waiting %.0fs for %lu job%s to terminate ...",
                  grace - cnt, (unsigned long) nrun, nrun == 1 ? "" : "s");
            rewindln(stderr);

            /* SIGCHLD interrupts sleep, so a simple countdown won't do */
            (void) sleep(1);
        }

        clearln(stderr);
        if (nrun > 0) {
            warnx("Killing %lu job%s ...",
                  (unsigned long) nrun, nrun == 1 ? "" : "s");
        } else {
            warnx("All jobs terminated");
        }

        (void) signaljobs(SIGKILL);

        errno = 0;
        if (sigprocmask(SIG_BLOCK, &oldmask, NULL) != 0) {
            warn("sigprocmask");
        }
    }
}


static void
clearln(FILE *const tty)
{
    /* cppcheck-suppress misra-c2012-14.4; return type is bool */
    if (isatty(fileno(tty))) {
        (void) fputs(CLEARLN, tty);
    }
}


static void
collect(const int signo __attribute__((unused)))
{
    const int olderr = errno;

    assert(signo == SIGCHLD);

    /* cppcheck-suppress misra-c2012-15.4; least bad solution */
    while (true) {
        pid_t pid;
        int status;

        /* cppcheck-suppress misra-c2012-15.4; least bad solution */
        while (true) {
            errno = 0;
            pid = waitpid(-1, &status, WNOHANG);

            if (pid > 0) {
                break;
            }

            if (pid == 0 || errno == ECHILD) {
                goto exit;
            }

            if (errno != EINTR) {
                waitpiderr = errno;
                goto exit;
            }
        }

        for (ptrdiff_t i = 0; i < maxnrun; ++i) {
            Job *job = &jobs[i];

            if (job->exited == 0 && pid == job->pid) {
                job->status = status;
                job->exited = 1;
                --nrun;
                goto next;
            }
        }

        /* NOTREACHED */
        unknownpid = pid;
        break;

        next:
            /* End of loop */;
    }

    exit:
        errno = olderr;
}


static long
getoptl(const long min, const long max)
{
    long num;

    errno = 0;
    num = strtol(optarg, NULL, BASE_TEN);

    if (num == 0 && errno != 0) {
        err(EXIT_FAILURE, "-%c", optopt);
    }
    if (num < min) {
        errx(EXIT_FAILURE, "-%c: %ld is smaller than %ld", optopt, num, min);
    }
    if (num > max) {
        errx(EXIT_FAILURE, "-%c: %ld is greater than %ld", optopt, num, max);
    }

    return num;
}


static time_t
now(void)
{
    return time(NULL);
}


static void
rewindln(FILE *const tty)
{
    /* cppcheck-suppress misra-c2012-14.4; return type is bool */
    if (isatty(fileno(tty))) {
        (void) fputs(REWINDLN, tty);
    }
}


static Error
putvar(ptrdiff_t *const nvars, char **const vars, char *const var)
{
    if (!var || !nvars || !vars) {
        return ERR_ARGS;
    }

    if (isdigit(var[0])) {
        return ERR_BAD;
    }

    char *ptr = var;
    while (*ptr == '_' || isalnum(*ptr)) {
        ++ptr;
    }

    if (*ptr != '=') {
        return ERR_BAD;
    }

    /* cppcheck-suppress misra-c2012-18.4; safe pointer subtraction */
    const ptrdiff_t len = ptr - var;

/* cppcheck-suppress misra-c2012-20.9; INT_MAX and PTRDIFF_T are defined */
#if PTRDIFF_MAX > SIZE_MAX
    if ((uintmax_t) len > (uintmax_t) SIZE_MAX) {
        return ERR_OVER;
    }
#endif

    ptrdiff_t ind = 0;
    while (ind < *nvars && strncmp(vars[ind], var, (size_t) len) != 0) {
        ++ind;
    }

    vars[ind] = var;
    if (ind == *nvars) {
        ++*nvars;
    }

    return OK;
}


static int
signaljobs(const int signo)
{
    int njobs = 0;
    for (ptrdiff_t i = 0; i < maxnrun; ++i) {
        const Job *job = &jobs[i];

        if (job->pid > 0 && job->exited == 0) {
            errno = 0;

            if (kill(job->pid, signo) == 0) {
                ++njobs;
            } else {
                warn("kill %lld", (long long) job->pid);
            }
        }
    }

    return njobs;
}


static Error
wordsadd(Words *const words, char *const word)
{
    if (!words || !words->vector || !word) {
        return ERR_ARGS;
    }

    const size_t count = words->count + 1U;

    /* The final 0 functions as terminator */
    if (count >= words->max) {
        if (SIZE_MAX / 2 < words->max) {
            return ERR_OVER;
        }

        const size_t newmax = words->max * 2U;

/* cppcheck-suppress misra-c2012-20.9; INT_MAX and PTRDIFF_T are defined */
#if SIZE_MAX > PTRDIFF_MAX
        if (newmax > PTRDIFF_MAX) {
            return ERR_OVER;
        }
#endif

        const Error retval = wordsrealloc(words, newmax);
        if (retval != OK) {
            return retval;
        }
    }

    words->vector[words->count] = word;
    words->count = count;

    return OK;
}


static Error
wordsfree(Words *const words)
{
    if (!words || !words->vector) {
        return ERR_ARGS;
    }

/* cppcheck-suppress misra-c2012-20.9; INT_MAX and PTRDIFF_T are defined */
#if SIZE_MAX > PTRDIFF_MAX
    if ((uintmax_t) words->count > PTRDIFF_MAX) {
        return ERR_BAD;
    }
#endif

    for (size_t i = 0; i < words->count; ++i) {
        char *const word = words->vector[i];

        if (word) {
            free(word);
        }
    }

    free(words->vector);
    words->vector = NULL;

    return OK;
}


static Error
wordsinit(Words *const words, const size_t count)
{
    if (!words || count == 0U) {
        return ERR_ARGS;
    }

    /* cppcheck-suppress misra-c2012-11.5; bad advice for calloc */
    words->vector = calloc(count, sizeof(char *));
    if (!words->vector) {
        return ERR_SYS;
    }

    words->count = 0;
    words->max = count;

    return OK;
}


static Error
wordsrealloc(Words *const words, const size_t count) {
    if (!words) {
        return ERR_ARGS;
    }

    if (count != words->max) {
        size_t newsize;
        char **tmp;

        if (SIZE_MAX / sizeof(char *) < count) {
            return ERR_OVER;
        }
        newsize = count * sizeof(char *);

        /* RATS: ignore; everything past the NULL terminator is irrelevant */
        tmp = realloc(words->vector, newsize);
        if (!tmp) {
            return ERR_SYS;
        }

        words->vector = tmp;
        words->max = count;
    }

    return OK;
}


static Error
wordsplit(Words *const words, const size_t slen, const char *const str)
{
    if (!str || !words) {
        return ERR_ARGS;
    }

    /* RATS: ignore; writes to quotes are bound-checked */
    char quotes[MAX_QUOT_NEST] = {0};
    unsigned short nquotes = 0;
    bool escape = false;
    size_t pos = 0;
    Error retval = ERR_UNSPEC;

    assert(slen < MAX_STR_SIZE);
    assert(strnlen(str, MAX_STR_SIZE) == slen);

    retval = wordsinit(words, MAX_NWORDS);
    if (retval != OK) {
        return retval;
    }

    if (slen == 0U) {
        return OK;
    }

    /* cppcheck-suppress misra-c2012-15.4; I cannot do fewer gotos */
    while (words->count < slen) {
        char *word = NULL;
        char *ptr = NULL;
        char *tmp = NULL;

        /* cppcheck-suppress misra-c2012-11.5; bad advice for calloc */
        word = calloc(slen + 1U, sizeof(char));
        if (!word) {
            (void) wordsfree(words);
            return ERR_SYS;
        }

        ptr = word;

        /* cppcheck-suppress misra-c2012-15.4; I cannot do fewer gotos */
        for (; pos <= slen; ++pos) {
            const char ch = str[pos];
            ptrdiff_t wlen = -1;

            if (escape) {
                *ptr = ch;
                ++ptr;
                escape = false;
            } else if (nquotes > 0U) {
                switch (ch) {
                case '\'':
                    /* Falls through */
                case '"':
                    if (quotes[nquotes - 1U] == ch) {
                        --nquotes;
                    }
                    else if (nquotes >= NELEMS(quotes)) {
                        retval = ERR_QUOTES;
                        goto error;
                    }
                    else {
                        quotes[nquotes] = ch;
                        ++nquotes;
                    }
                    break;
                case '\\':
                    escape = true;
                    break;
                default:
                    *ptr = ch;
                    ++ptr;
                    break;
                }
            } else {
                switch (ch) {
                case '\\':
                    escape = true;
                    break;
                case '\'':
                    /* Falls through */
                case '"':
                    quotes[nquotes] = ch;
                    ++nquotes;
                    break;
                case ' ':
                    /* Falls through */
                case '\t':
                    /* Falls through */
                case '\0':
                    /* cppcheck-suppress misra-c2012-18.4; is safe */
                    wlen = ptr - word;
                    if (wlen < 0) {
                        retval = ERR_SIGN;
                        goto error;
                    }
                    if ((uintmax_t) wlen >= (uintmax_t) SIZE_MAX) {
                        retval = ERR_OVER;
                        goto error;
                    }

                    /* cppcheck-suppress misra-c2012-11.5; does not apply */
                    tmp = realloc(word, (size_t) wlen + 1U); /* RATS: ignore */
                    if (tmp) {
                        word = tmp;
                    }

                    if (wordsadd(words, word) != OK) {
                        retval = ERR_SYS;
                        goto error;
                    }

                    /* cppcheck-suppress misra-c2012-14.2; not a clean loop */
                    pos += strspn(&str[pos], " \t");
                    if (str[pos] == '\0') {
                        goto exit;
                    } else {
                        goto next;
                    }
                    break;
                case '#':
                    break;
                default:
                    *ptr = ch;
                    ++ptr;
                    break;
                }
            }
        }

        /* Only reached if quotes are unbalanced */
        retval = ERR_SYNTAX;

        error:
            free(word);
            (void) wordsfree(words);
            return retval;

        next:
            /* End of loop */;
    }

    /* NOTREACHED */
    (void) wordsfree(words);
    return ERR_NELEMS;

    exit:
        (void) wordsrealloc(words, words->count + 1U);
        return OK;
}
